/*
 * Copyright (C) Francisco Morero Peyrona. All rights reserved.
 *
 * This software is published under the terms of Open Source
 * License version 1.1, a copy of which has been included with this
 * distribution in the License.txt file.
 */

package jdbcmanager.swing.text;

import java.util.*;

import javax.swing.text.*;

/**
 * This is the base class for all Styled documents that handle source code in any language.
 * <p>
 * Note: most part of this class is based in someone else code, but I can not thank him because 
 * I found the code without the author name. If you know him, please contact with me.
 * 
 * @author Francisco Morero Peyrona
 */
// NEXT: que reconozca los numeros y los pinte con sus atributos
public abstract class CodeStyledDocument extends DefaultStyledDocument
{   
   private Hashtable htKeywords = new Hashtable();
   private Element   rootElement;
   private boolean   bMultiLineComment;

   //-------------------------------------------------------------------------//
   
   /**
    * Class constructor
    */
   public CodeStyledDocument()
   {  
      this.rootElement = getDefaultRootElement();
      
      putProperty( DefaultEditorKit.EndOfLineStringProperty, "\n" );
   }
   
   //-------------------------------------------------------------------------//
   // PUBLIC
   //-------------------------------------------------------------------------//

   /**
    * Override to apply syntax highlighting after the document has been updated
    */
   public void insertString( int offset, String str, AttributeSet a )   throws BadLocationException
   {
      super.insertString( offset, str, a );
      processChangedLines( offset, str.length() );
   }

   /**
    *  Override to apply syntax highlighting after the document has been updated
    */
   public void remove( int offset, int length )  throws BadLocationException
   {
      super.remove( offset, length );
      processChangedLines( offset, 0 );
   }
   
   //-------------------------------------------------------------------------//
   // PROTECTED
   //-------------------------------------------------------------------------//

   /**
    * Add a keyword.
    *   
    * @param sKeyword Keyword to be added  (it will be converted to lower case)
    * @param sas
    */
   protected void addKeyword( String sKeyword, SimpleAttributeSet sas )
   {
      this.htKeywords.put( sKeyword.toLowerCase(), sas );
   }

   /**
    * Remove a keyword.
    *   
    * @param sKeyword Keyword to be removed (it will be converted to lower case)
    */
   protected void removeKeyword( String sKeyword )
   {
      this.htKeywords.remove( sKeyword.toLowerCase() );
   }
      
   /**
    * @return
    */
   protected abstract SimpleAttributeSet getCommentAttributeSet();

   /**
    * @return
    */
   protected abstract SimpleAttributeSet getNormalAttributeSet();

   /**
    * @return
    */
   protected abstract SimpleAttributeSet getNumberAttributeSet();

   /**
    * @return
    */
   protected abstract SimpleAttributeSet getQuoteAttributeSet();

   /**
    * @return
    */
   protected abstract String getQuoteDelimiters();
   
   /**
    * @return
    */
   protected abstract String getSingleLineComment();
   
   /**
    * 
    * @return
    */
   protected abstract String getOpenMultiLineComment();
   
   /**
    * 
    * @return
    */
   protected abstract String getCloseMultiLineComment();
   
   /**
    * @return
    */
   protected abstract String getTokenDelimiters();

   /**
    * 
    * @param sQuoteDelimiter
    * @return
    */
   protected abstract String getEscapeString( String sQuoteDelimiter );
   
   //-------------------------------------------------------------------------//
   // PRIVATES
   
   // This is just to simplify things 
   private boolean isDelimiter( String character )
   {
      return getTokenDelimiters().indexOf( character ) != -1;
   }
   
   // This is just to simplify things
   private boolean isQuoteDelimiter( String character )
   {
      return getQuoteDelimiters().indexOf( character ) >= 0;
   }

   private SimpleAttributeSet getKeywordAttribute( String token )
   {
      return (SimpleAttributeSet) this.htKeywords.get( token.toLowerCase() );
   }
   
   /*
    *  Determine how many lines have been changed,
    *  then apply highlighting to each line
    */
   private void processChangedLines( int offset, int length )  throws BadLocationException
   {
      String content = getText( 0, getLength() );

      //  The lines affected by the latest document update

      int startLine = rootElement.getElementIndex( offset );
      int endLine = rootElement.getElementIndex( offset + length );

      //  Make sure all comment lines prior to the start line are commented
      //  and determine if the start line is still in a multi line comment

      setMultiLineComment( commentLinesBefore( content, startLine ) );

      //  Do the actual highlighting

      for( int i = startLine; i <= endLine; i++ )
         applyHighlighting( content, i );

      //  Resolve highlighting to the next end multi line delimiter

      if( isMultiLineComment() )
         commentLinesAfter( content, endLine );
      else
         highlightLinesAfter( content, endLine );
   }

   /*
    *  Highlight lines when a multi line comment is still 'open'
    *  (ie. matching end delimiter has not yet been encountered)
    */
   private boolean commentLinesBefore( String content, int line )
   {
      int offset = rootElement.getElement( line ).getStartOffset();

      //  Start of comment not found, nothing to do

      int startDelimiter = lastIndexOf( content, getOpenMultiLineComment(), offset - 2 );

      if( startDelimiter < 0 )
         return false;

      //  Matching start/end of comment found, nothing to do

      int endDelimiter = indexOf( content, getCloseMultiLineComment(), startDelimiter );

      if( endDelimiter < offset & endDelimiter != -1 )
         return false;

      //  End of comment not found, highlight the lines

      setCharacterAttributes( startDelimiter, offset - startDelimiter + 1, getCommentAttributeSet(), false );
   
      return true;
   }

   /*
    *  Highlight comment lines to matching end delimiter
    */
   private void commentLinesAfter( String content, int line )
   {
      int offset = rootElement.getElement( line ).getEndOffset();

      //  End of comment not found, nothing to do

      int endDelimiter = indexOf( content, getCloseMultiLineComment(), offset );

      if( endDelimiter < 0 )
         return;

      //  Matching start/end of comment found, comment the lines

      int startDelimiter = lastIndexOf( content, getOpenMultiLineComment(), endDelimiter );

      if( startDelimiter < 0 || startDelimiter <= offset )
         setCharacterAttributes( offset, endDelimiter - offset + 1, getCommentAttributeSet(), false );
   }

   /*
    *  Highlight lines to start or end delimiter
    */
   private void highlightLinesAfter( String content, int line )
   {
      int offset = rootElement.getElement( line ).getEndOffset();

      //  Start/End delimiter not found, nothing to do

      int startDelimiter = indexOf( content, getOpenMultiLineComment() , offset );
      int endDelimiter   = indexOf( content, getCloseMultiLineComment(), offset );

      if( startDelimiter < 0 )
         startDelimiter = content.length();

      if( endDelimiter < 0 )
         endDelimiter = content.length();

      int delimiter = Math.min( startDelimiter, endDelimiter );

      if( delimiter < offset )
         return;

      // Start/End delimiter found, reapply highlighting

      int endLine = rootElement.getElementIndex( delimiter );

      for( int i = line + 1; i < endLine; i++ )
      {
         Element      branch = rootElement.getElement( i );
         Element      leaf   = getCharacterElement( branch.getStartOffset() );
         AttributeSet as     = leaf.getAttributes();

         if( as.isEqual( getCommentAttributeSet() ) )
            applyHighlighting( content, i );
      }
   }

   /*
    *  Parse the line to determine the appropriate highlighting
    */
   private void applyHighlighting( String content, int line )
   {
      int startOffset = rootElement.getElement( line ).getStartOffset();
      int endOffset = rootElement.getElement( line ).getEndOffset() - 1;

      int lineLength = endOffset - startOffset;
      int contentLength = content.length();

      if( endOffset >= contentLength )
         endOffset = contentLength - 1;

      //  check for multi line comments
      //  (always set the comment attribute for the entire line)

      if( endingMultiLineComment( content, startOffset, endOffset )
          || isMultiLineComment()
          || startingMultiLineComment( content, startOffset, endOffset ) )
      {
         setCharacterAttributes( startOffset, endOffset - startOffset + 1, getCommentAttributeSet(), false );
         return;
      }

      //  set normal attributes for the line
      setCharacterAttributes( startOffset, lineLength, getNormalAttributeSet(), true );

      //  check for single line comment
      int index = content.indexOf( getSingleLineComment(), startOffset );

      if( ( index > -1 ) && ( index < endOffset ) )
      {
         setCharacterAttributes( index, endOffset - index + 1, getCommentAttributeSet(), false );
         endOffset = index - 1;
      }

      //  check for tokens
      checkForTokens( content, startOffset, endOffset );
   }

   /*
    *  Does this line contain the start delimiter
    */
   private boolean startingMultiLineComment( String content, int startOffset, int endOffset )
   {
      int index = indexOf( content, getOpenMultiLineComment(), startOffset );

      if( (index < 0) || (index > endOffset) )
         return false;
      
      setMultiLineComment( true );
      return true;
   }

   /*
    *  Does this line contain the end delimiter
    */
   private boolean endingMultiLineComment( String content, int startOffset, int endOffset )
   {
      int index = indexOf( content, getCloseMultiLineComment(), startOffset );

      if( (index < 0) || (index > endOffset) )
         return false;

      setMultiLineComment( false );
      return true;
   }

   /*
    *  We have found a start delimiter
    *  and are still searching for the end delimiter
    */
   private boolean isMultiLineComment()
   {
      return this.bMultiLineComment;
   }

   private void setMultiLineComment( boolean value )
   {
      this.bMultiLineComment = value;
   }

   /*
    * Parse the line for tokens to highlight
    */
   private void checkForTokens( String content, int startOffset, int endOffset )
   {
      while( startOffset <= endOffset )
      {
         //  skip the delimiters to find the start of a new token
         while( isDelimiter( content.substring( startOffset, startOffset + 1 ) ) )
         {
            if( startOffset < endOffset )
               startOffset++;
            else
               return;
         }

         //  Extract and process the entire token
         if( isQuoteDelimiter( content.substring( startOffset, startOffset + 1 ) ) )
            startOffset = getQuoteToken( content, startOffset, endOffset );
         else
            startOffset = getOtherToken( content, startOffset, endOffset );
      }
   }

   /*
    *
    */
   private int getQuoteToken( String content, int startOffset, int endOffset )
   {
      String quoteDelimiter = content.substring( startOffset, startOffset + 1 );
      String escapeString = getEscapeString( quoteDelimiter );

      int index;
      int endOfQuote = startOffset;

      // skip over the escape quotes in this quote
      index = content.indexOf( escapeString, endOfQuote + 1 );

      while( ( index > -1 ) && ( index < endOffset ) )
      {
         endOfQuote = index + 1;
         index = content.indexOf( escapeString, endOfQuote );
      }

      // now find the matching delimiter
      index = content.indexOf( quoteDelimiter, endOfQuote + 1 );

      if( ( index < 0 ) || ( index > endOffset ) )
         endOfQuote = endOffset;
      else
         endOfQuote = index;

      setCharacterAttributes( startOffset, endOfQuote - startOffset + 1, getQuoteAttributeSet(), false );

      return endOfQuote + 1;
   }

   /*
    *
    */
   private int getOtherToken( String content, int startOffset, int endOffset )
   {
      int endOfToken = startOffset + 1;

      while( endOfToken <= endOffset )
      {
         if( isDelimiter( content.substring( endOfToken, endOfToken + 1 ) ) )
            break;

         endOfToken++;
      }

      String       token = content.substring( startOffset, endOfToken );
      AttributeSet attr  = getKeywordAttribute( token );

      if( attr != null )
         setCharacterAttributes( startOffset, endOfToken - startOffset, attr, false );

      return endOfToken + 1;
   }

   /*
    *  Assume the needle will the found at the start/end of the line
    */
   private int indexOf( String content, String needle, int offset )
   {
      int index;

      while( ( index = content.indexOf( needle, offset ) ) != -1 )
      {
         String text = getLine( content, index ).trim();

         if( text.startsWith( needle ) || text.endsWith( needle ) )
            break;
         
         offset = index + 1;
      }

      return index;
   }

   /*
    *  Assume the needle will the found at the start/end of the line
    */
   private int lastIndexOf( String content, String needle, int offset )
   {
      int index;

      while( ( index = content.lastIndexOf( needle, offset ) ) != -1 )
      {
         String text = getLine( content, index ).trim();

         if( text.startsWith( needle ) || text.endsWith( needle ) )
            break;
         
         offset = index - 1;
      }

      return index;
   }

   private String getLine( String content, int offset )
   {
      int line = rootElement.getElementIndex( offset );
      Element lineElement = rootElement.getElement( line );
      int start = lineElement.getStartOffset();
      int end = lineElement.getEndOffset();
      return content.substring( start, end - 1 );
   }   
}
